package {
	
	import flash.utils.*;
	
	import mx.collections.ArrayCollection;
	import mx.collections.ICollectionView;
	import mx.utils.ArrayUtil;
	
	[RemoteClass(alias="MightyArray")]
	/**
	 * Improved descendant of native AS3 Array, event-less architecture,
	 * fast and powerful replacement for standard mx.collections.ArrayCollection
	 *  
	 * @see mx.collections.ArrayCollection
	 * @see Array
	 * @see Property 
	 */
	public dynamic class MightyArray extends Array implements IExternalizable {
		
		public static const DATA_PROVIDER:String = "dataProvider"; 
		
		public var strict:Boolean = true;
		
		protected var _owner:Object;		

		function MightyArray(source:* = null, strict:Boolean = true) {
			this.strict = strict;			
			append(source);
		}
		
		/**
		 * owner - property intended to hold a reference to visual control having 
		 * dataProvider property, usually this could be DataGrid, Tree or any List descendant, etc.
		 * Aim to synchronize Visual control with MightyArray changes 
		 */
		public function get owner():Object {
			return _owner;
		}
		
		public function set owner(value:Object):void {
			if (value && value.hasOwnProperty(DATA_PROVIDER)) {
				_owner = value;
				owner[DATA_PROVIDER] = this;
			}
		}
		
		/**
		 *  Get the item at the specified index.
		 * 
		 *  @param 	index the index in the list from which to retrieve the item
		 *  @param	prefetch int indicating both the direction and amount of items
		 *			to fetch during the request should the item not be local.
		 *  @return the item at that index, null if there is none
		 *  @throws RangeError if the index < 0 or index >= length
		 */
		public function getItemAt(index:int, prefetch:int = 0):Object {
			
			if (index < 0 || index >= this.length) {
				var message:String = "rangeError";
				throw new RangeError(message);
			}
			
			return this[index];
		}
		
		/**
		 * Get the item having specified property
		 *  
		 * @param instance of Property class
		 * @return the first item having identical property as passed to parameter, null if there is none
		 * 
		 * @see Property
		 */
		public function getItemByProperty(property:Property):Object {
			var item:Object;
			some(
				function(element:*, index:int, arr:Array):Boolean {
					var bool:Boolean = element.hasOwnProperty(property.name) && element[property.name] === property.value;
					
					if (element is XML)
						bool = bool || (String(element[property.name]) === property.value);
						
					if (bool) item = element;
					return bool;
				}				
				, this
			);
			return item; 
			
		}
		
		/**
		 * Search for matches in array
		 * 
		 * @param item Property class instance OR Object reference
		 * @return <code>true</code> if item has been found, <code>false</code> otherwise.
		 * 
		 * @see Property
		 */
		public function contains(item:Object):Boolean {
			
			if (item is Property)
				return getItemByProperty(Property(item)) != null;
			
			if (item is String)
				for each (var i:String in this)
					if (i == item)
						return true;
			
			return getItemIndex(item) != -1;
		}		
		
		/**
		 *  Add the specified item to the end of the list.
		 *  Equivalent to addItemAt(item, length);
		 * 
		 *  @param item the item to add
		 */		
		public function addItem(item:Object, refresh:Boolean = true):void {
			
			if (strict && contains(item)) 
				return;
				
			this.push(item);
			
			if (refresh)
				this.refresh();
		}
		
		/**
		 *  Add the item at the specified index.  
		 *  Any item that was after this index is moved out by one.  
		 * 
		 *  @param item the item to place at the index
		 *  @param index the index at which to place the item
		 *  @throws RangeError if index is less than 0 or greater than the length
		 */		
		public function addItemAt(item:Object, index:int, refresh:Boolean = true):void {
			
			if (index < 0 || index > length) {
				var message:String = "rangeError";
				throw new RangeError(message);
			}
			
			if (strict && contains(item)) 
				return;			
			
			this.splice(index, 0, item);
			if (refresh)
				this.refresh();
		}
		
		/**
		 *  Return the index of the item if it is in the list such that
		 *  getItemAt(index) == item.  
		 *  Note that in this implementation the search is linear and is therefore 
		 *  O(n).
		 * 
		 *  @param item the item to find
		 *  @return the index of the item, -1 if the item is not in the list.
		 */
		public function getItemIndex(item:Object):int {
			
			if (item is Property) {
				var result:int = -1;
				var p:Property = Property(item);
				some(
					function(element:*, index:int, arr:Array):Boolean {
						var bool:Boolean = element.hasOwnProperty(p.name) && element[p.name] === p.value;
						if (bool) result = index;
						return bool;
					}				
					, this
				);				
				return result;
			}
			
			if (item is String) {
				var idx:int = -1;
				for each (var i:String in this) {
					idx++;
					if (i == item)
						return idx;
				}	
			}		
			
			return ArrayUtil.getItemIndex(item, this);
		}
		
		/**
		 *  Removes the specified item from this list, should it exist.
		 *
		 *	@param	item Object reference to the item that should be removed.
		 *  @return	Boolean indicating if the item was removed.
		 */
		public function removeItem(item:Object, refresh:Boolean = true):Boolean	{
			var result:Boolean = contains(item);
			if (result) {
				var idx:int = getItemIndex(item);
				
				if (idx >=0 && idx < length)
					removeItemAt(idx);
				
				if (refresh)
					this.refresh();
			}
			
			return result;
		}
		
		/**
		 *  Remove the item at the specified index and return it.  
		 *  Any items that were after this index are now one index earlier.
		 *
		 *  @param index the index from which to remove the item
		 *  @return the item that was removed
		 *  @throws RangeError is index < 0 or index >= length
		 */
		public function removeItemAt(index:int, refresh:Boolean = true):Object {
			if (index < 0 || index >= length) {
				var message:String = "rangeError";
				throw new RangeError(message);
			}
			
			var removed:Object = this.splice(index, 1)[0];
			
			if (removed && refresh)
				this.refresh();
			
			return removed;
		}
		
		/** 
		 *  Remove all items from the list.
		 */
		public function removeAll():void {
			if (length > 0) {	
				splice(0, length);				
				refresh();
			}
		}		

		/** 
		 *  Remove all items from the list that are on the parameter array and return it.
		 *  Method can use parameter property to examines objects on equality.
		 *
		 *  @param index the index from which to remove the item
		 * 	@param property that is used to 
		 *  @return removed items
		 */
		public function intersect(array:MightyArray, property:Property = null):MightyArray {
			var result:MightyArray = new MightyArray();

			for each (var o:Object in array)
				if ((property && contains(new Property(property.name, o[property.name]))) ||
					(!property && contains(o))) {
						result.addItem(o);
						removeItem(o);
					}

			return result;
		}

		/**
		 * Filters out items having certain property state
		 *  
		 * @param property - Property class instance
		 * @return MightyArray of items
		 * 
		 * @see Property
		 */
		public function filterByProperty(property:Property):MightyArray {
			return new MightyArray(
						filter(
							function(element:*, index:int, arr:Array):Boolean {
								var result:Boolean = element.hasOwnProperty(property.name) 
														&& element[property.name] === property.value; 
								
								if (element is XML)
									result = result || (String(element[property.name]) === property.value);								
								
								return result;
							}								
							, this
						)
					);
		}
		
		/**
		 * Update Visual owner with any changes might occured before  
		 */
		public function refresh():Boolean {
			if (owner)
				return ICollectionView(owner[DATA_PROVIDER]).refresh();
			return false;
		}
		
		/**
		 *  Return an Array that is populated in the same order as the ArrayCollection
		 *  implementation.  
		 * 
		 */ 
		public function toArray():Array	{
			return this.concat();
		}
		
		public function toPropertyValueList(propertyName:String):MightyArray {
			var result:MightyArray = new MightyArray();
			
			for each (var o:* in this)
				if (o is XML)
					result.addItem(String(o[propertyName]))
				else
					result.addItem(o[propertyName]);
			
			return result;
		}
		
		/**
		 * Convert MightyArray to dictionary with specified key 
		 * @param propertyKey the key for dictionary
		 * @return dictionary contains MightyArray elements
		 * 
		 * @see Dictionary;
		 */		 		
		public function toDictionary(propertyKey:String = ''):Dictionary	{
			var result:Dictionary = new Dictionary(true);
			
			for each (var obj:Object in this)
				if (obj.hasOwnProperty(propertyKey))
					if (obj is XML)
						result[String(obj[propertyKey])] = obj
					else	
						result[obj[propertyKey]] = obj
				else
					result[obj] = obj;
			
			return result;
		}		

		public function clone():MightyArray	{
			return new MightyArray(toArray());
		}
		
		public function sortByField(fieldName:String, caseInsensitive:Boolean = true, descending:Boolean = false, numeric:Boolean = false):void {
			var opt:int = 0;
			
			if (caseInsensitive)
				opt |= Array.CASEINSENSITIVE;
				
			if (descending)
				opt |= Array.DESCENDING;
			
			if (numeric)
				opt |= Array.NUMERIC;

			sortOn(fieldName, opt);
			
			refresh();
		}		
		
		/**
		 * Appends items from source provider to internal items list 
		 * @param source, can be one of: Array, XMLList, ArrayCollection, Dictionary
		 * 
		 * @see Array
		 * @see Dictionary 
		 */
		public function append(source:*):MightyArray {
			
			// Supported sources to append
			if (!source ||
				!(source is Array) &&
				!(source is XMLList) && 
				!(source is ArrayCollection) &&
				!(source is Dictionary))
				return this;
			
			for each (var o:* in source)
				addItem(o);
				
			return this;
		}
		
		public function readExternal(input:IDataInput):void	{
			append(input.readObject() as MightyArray);
		}
		
		public function writeExternal(output:IDataOutput):void {
			output.writeObject(this);
		}

	}
}